package org.infodavid.common.impl.services;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

import javax.persistence.PersistenceException;
import javax.security.auth.login.AccountException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import org.infodavid.common.dto.IUserConnectionListItem;
import org.infodavid.common.dto.IUserListItem;
import org.infodavid.common.dto.IUserSessionListItem;
import org.infodavid.common.dto.IUserGroupListItem.EUserRole;
import org.infodavid.common.impl.model.criteria.UserSessionCriteriaImpl;
import org.infodavid.common.impl.services.helpers.ServiceHelper;
import org.infodavid.common.impl.services.security.DefaultPasswordPolicy;
import org.infodavid.common.model.IGenericProperty;
import org.infodavid.common.model.IUser;
import org.infodavid.common.model.IUserConnection;
import org.infodavid.common.model.IUserGroup;
import org.infodavid.common.model.IUserSession;
import org.infodavid.common.model.behaviors.IApprovable.EApprovalStatus;
import org.infodavid.common.model.components.IEMail;
import org.infodavid.common.model.criteria.IExtendedSearchCriteria;
import org.infodavid.common.model.criteria.IGenericPropertyCriteria;
import org.infodavid.common.model.criteria.IUserConnectionCriteria;
import org.infodavid.common.model.criteria.IUserCriteria;
import org.infodavid.common.model.criteria.IUserSessionCriteria;
import org.infodavid.common.model.impl.criteria.ExtendedCriteriaImpl;
import org.infodavid.common.net.NetUtil;
import org.infodavid.common.notifier.exceptions.NotifierException;
import org.infodavid.common.persistence.ITransaction;
import org.infodavid.common.persistence.PersistenceSession;
import org.infodavid.common.persistence.UserConnectionDataService;
import org.infodavid.common.persistence.UserDataService;
import org.infodavid.common.persistence.UserGroupDataService;
import org.infodavid.common.persistence.UserSessionDataService;
import org.infodavid.common.persistence.ILockable.ELockType;
import org.infodavid.common.persistence.exceptions.NoDataFoundException;
import org.infodavid.common.services.ApplicationSettingsService;
import org.infodavid.common.services.IApplicationContext;
import org.infodavid.common.services.IPasswordPolicy;
import org.infodavid.common.services.UserService;
import org.infodavid.common.services.exception.AccessDeniedException;
import org.infodavid.common.services.exception.IllegalOperationException;
import org.infodavid.common.services.exception.UserAccountExpiredException;
import org.infodavid.common.services.exception.UserAccountLockedException;
import org.infodavid.common.services.exception.UserSessionExpiredException;
import org.infodavid.common.services.exceptions.ServiceException;
import org.infodavid.common.services.helpers.NotificationHelper;
import org.infodavid.common.services.validators.IValidator;
import org.infodavid.common.util.CollectionsUtil;
import org.infodavid.common.util.IVisitor;

/**
 * The Class AbstractUserService.
 */
public abstract class AbstractUserService extends UserService {

  /** The Constant log. */
  private static final Log LOGGER = LogFactory.getLog(AbstractUserService.class);

  /**
   * The Class RemovalValidator.
   */
  protected static class RemovalValidator implements IValidator<IUser> {

    /*
     * See super class or interface. (non-Javadoc)
     * @see org.infodavid.common.services.validators.IValidator#validate(org.infodavid .common.services.
     * IApplicationContext, java.io.Serializable)
     */
    public void validate(final IApplicationContext context, final IUser value)
        throws ServiceException, PersistenceException {
      if (value != null) {
        if (IUser.ADMINISTRATOR.equals(value.getName())) {
          throw new IllegalOperationException("User can not be removed: " + value.getName());
        }
        else if (IUser.SYSTEM.equals(value.getName())) {
          throw new IllegalOperationException("User can not be removed: " + value.getName());
        }
      }
    }

    /*
     * See super class or interface. (non-Javadoc)
     * @see org.infodavid.common.services.validators.IValidator#validate(org.infodavid .common.services.
     * IApplicationContext, java.util.Collection)
     */
    public void validate(final IApplicationContext context, final Collection<IUser> values)
        throws ServiceException, PersistenceException {
      if (!(values == null || values.isEmpty())) {
        for (final IUser value : values) {
          validate(context, value);
        }
      }
    }
  }

  /**
   * The Class UserSessionsRemovalVisitor.
   */
  private static class UserSessionsRemovalVisitor implements
      IVisitor<Collection<IUserSessionListItem>,Object> {

    /** The service. */
    private final UserSessionDataService service = UserSessionDataService.getInstance();

    /** The context. */
    private final IApplicationContext context;

    /** The flag. */
    private final boolean isDebugEnabled = LOGGER.isDebugEnabled();

    /**
     * Instantiates a new user sessions removal visitor.
     * @param arg0 the arg0
     */
    public UserSessionsRemovalVisitor(final IApplicationContext arg0) {
      context = arg0;
    }

    /*
     * (non-Javadoc)
     * @see org.infodavid.common.util.IVisitor#initialize()
     */
    public void initialize() throws Exception {}

    /*
     * (non-Javadoc)
     * @see org.infodavid.common.util.IVisitor#visit(java.lang.Object)
     */
    @SuppressWarnings({
        "unchecked", "rawtypes"
    })
    @Override
    public IResult visit(final Collection<IUserSessionListItem> data) throws Exception {
      IResult result;

      if (data == null || data.isEmpty()) {
        result = IVisitor.SUCCESS;

        if (isDebugEnabled) {
          LOGGER.debug("No expired user session found.");
        }
      }
      else {
        LOGGER.info("Removing " + data.size() + " expired user sessions.");

        final PersistenceSession persistenceSession = context.getPersistenceSession();
        result = IVisitor.SUCCESS;

        for (final IUserSessionListItem item : data) {
          if (isDebugEnabled) {
            LOGGER.debug("Removing expired user session for user: " + item.getUser());
          }

          // TODO some stuff

          service.removeEntity(persistenceSession, item);
        }
      }

      return result;
    }

  }

  /** The removal validator. */
  private final IValidator<IUser> removalValidator = new RemovalValidator();

  /** The helper. */
  private final ServiceHelper<IUserListItem,IUser,Long,IUserCriteria> helper;

  /** The helper. */
  private final ServiceHelper<IUserConnectionListItem,IUserConnection,Long,IUserConnectionCriteria> connectionHelper;

  /** The helper. */
  private final ServiceHelper<IUserSessionListItem,IUserSession,String,IUserSessionCriteria> sessionHelper;

  /** The password policy. */
  private IPasswordPolicy passwordPolicy = new DefaultPasswordPolicy(5, 64);

  /**
   * The Constructor.
   */
  protected AbstractUserService() {
    super();

    helper =
        new ServiceHelper<IUserListItem,IUser,Long,IUserCriteria>(UserDataService.getInstance());
    connectionHelper =
        new ServiceHelper<IUserConnectionListItem,IUserConnection,Long,IUserConnectionCriteria>(
            UserConnectionDataService.getInstance());
    sessionHelper =
        new ServiceHelper<IUserSessionListItem,IUserSession,String,IUserSessionCriteria>(
            UserSessionDataService.getInstance());
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.UserService#getPasswordPolicy()
   */
  @Override
  public IPasswordPolicy getPasswordPolicy() {
    return passwordPolicy;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#getLocale(org.infodavid.common.
   * services.IApplicationContext , org.infodavid.common.model.IUser)
   */
  @Override
  public Locale getLocale(final IApplicationContext context, final IUser user)
      throws ServiceException, PersistenceException {
    final Locale result;

    if (user.getLocale() == null) {
      result = ApplicationSettingsService.getInstance().getDefaultLocale(context);
    }
    else {
      result = user.getLocale();
    }

    return result;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#getTimeZone(org.infodavid.common
   * .services.IApplicationContext , org.infodavid.common.model.IUser)
   */
  @Override
  public DateTimeZone getTimeZone(final IApplicationContext context, final IUser user)
      throws ServiceException, PersistenceException {
    final DateTimeZone result;

    if (user.getLocale() == null) {
      result = ApplicationSettingsService.getInstance().getDefaultTimeZone(context);
    }
    else {
      result = user.getTimeZone();
    }

    return result;
  }

  /**
   * Sets the password policy.
   * @param value the password policy
   */
  public void setPasswordPolicy(final IPasswordPolicy value) {
    passwordPolicy = value;
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.AbstractEntityService#add(org.infodavid.common .services.
   * IApplicationContext, org.infodavid.common.model.IDataObject)
   */
  @Override
  public long add(final IApplicationContext context, final IUser value)
      throws PersistenceException, ServiceException {
    helper.validate(context, getValidator(), value);

    final IInterceptorEvent evt = preExecute(context, ADD_ACTION, value);

    // we add the guest groups
    addGuestsGroups(context, value);

    final long key = helper.add(context, value).longValue();

    postExecute(evt.setResults(value));

    LOGGER.info("A new user has been added (" + key + ')');

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("A new user has been added: " + value + " (" + key + ')');
    }

    return key;
  }

  /**
   * Initialize the system data if necessary.
   * @param session the persistence session
   * @return the users
   * @throws PersistenceException the persistence exception
   * @throws ServiceException the service exception
   */
  protected Map<String,IUser> addSystemDataToDbSchema(final PersistenceSession session)
      throws PersistenceException, ServiceException {
    // We force use of default email and password for administrator
    return ServiceUtil.initializeSystemData(session, null, null);
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#findSystemUser(org.infodavid.common .services.
   * IApplicationContext)
   */
  @Override
  public IUser findSystemUser(final IApplicationContext context) throws ServiceException,
      PersistenceException {
    final IExtendedSearchCriteria criteria = new ExtendedCriteriaImpl();
    final List<IUser> users = new ArrayList<IUser>();

    criteria.setPageStart(Long.valueOf(0));
    criteria.setPageSize(Integer.valueOf(1));
    UserDataService.getInstance().findByRole(context.getPersistenceSession(), EUserRole.SYSTEM,
        users, criteria);

    return users.isEmpty() ? null : users.get(0);
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#authentication(java.util.Map)
   */
  @Override
  public IApplicationContext authentication(final Map<String,Serializable> properties)
      throws ServiceException, PersistenceException {
    final PersistenceSession session = getPersistenceSession(properties);
    final DefaultApplicationContext context = new DefaultApplicationContext(properties, session);
    IUser user = findSystemUser(context);

    if (user == null) {
      final Map<String,IUser> addedUsers = addSystemDataToDbSchema(session);
      user = addedUsers.get(IUser.SYSTEM);

      if (user == null) {
        throw new AccessDeniedException("User is not valid");
      }
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("System user authenticated = " + user);
    }

    user.setHighestRole(EUserRole.SYSTEM);
    context.setUser(user);

    return context;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#authenticate(java.util.Map, java.lang.Long)
   */
  @Override
  public IApplicationContext authenticate(final Map<String,Serializable> properties,
      final Long userKey) throws ServiceException, PersistenceException {
    if (userKey == null) {
      throw new IllegalArgumentException(IUser.KEY);
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Starting user authentication using the user key: " + userKey);
    }

    if (userKey.longValue() < 0) {
      throw new IllegalArgumentException(IUser.KEY + ':' + userKey);
    }

    final PersistenceSession session = getPersistenceSession(properties);
    final DefaultApplicationContext context = new DefaultApplicationContext(session);
    final IUser user = UserDataService.getInstance().findByPrimaryKey(session, userKey);

    if (user == null) {
      throw new AccessDeniedException("User is not valid");
    }

    user.setHighestRole(findHighestRole(context, userKey.longValue()));

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("User authenticated = " + user);
    }

    context.setUser(user);

    return context;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#authenticate(java.util.Map, java.lang.String)
   */
  @Override
  public IApplicationContext authenticate(final Map<String,Serializable> properties,
      final String sessionKey) throws ServiceException, PersistenceException, AccountException {
    if (sessionKey == null) {
      throw new IllegalArgumentException(IUserSession.KEY);
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Starting user authentication using the session key: " + sessionKey);
    }

    if (StringUtils.isEmpty(sessionKey)) {
      throw new IllegalArgumentException(IUserSession.KEY + ':' + sessionKey);
    }

    final PersistenceSession persistenceSession = getPersistenceSession(properties);
    // ensure transaction is started
    final ITransaction transaction = persistenceSession.getTransaction();
    final boolean restartTransaction = transaction.isActive();

    // we need to start a transaction
    if (!restartTransaction) {
      transaction.begin();
    }
    final DefaultApplicationContext context =
        new DefaultApplicationContext(properties, persistenceSession);

    final IUserSession userSession =
      UserSessionDataService.getInstance().findByPrimaryKey(persistenceSession, sessionKey,
          ELockType.WAIT);

    try {
      if (userSession == null) {
        throw new UserSessionExpiredException(IUserConnection.KEY + ':' + sessionKey);
      }

      final UserSessionDataService userSessionService = UserSessionDataService.getInstance();

      final String ip = getLastIP(properties);

      /**
       * removed, refer to defect 376 if (userSession.getLastIP() != null && ip != null &&
       * !ip.equals(userSession.getLastIP())) { throw new AccessDeniedException(IUserSession.LAST_IP
       * + ':' + ip); }
       */

      userSession.setAccessesCount(userSession.getAccessesCount() + 1);
      userSession.setLastAccessDate(new DateTime());
      userSession.setLastIP(ip);

      // updating user session
      userSessionService.update(persistenceSession, userSession);

    }
    finally {
      transaction.commit();

      if (restartTransaction) {
        transaction.begin();
      }
    }

    context.setUserSession(userSession);
    context.setUser(userSession.getUser());
    context.getUser().setHighestRole(
        findHighestRole(context, context.getUser().getKey().longValue()));

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("User session updated = " + userSession.getKey());
      LOGGER.debug("User authenticated = " + userSession.getUser());
    }

    return context;
  }

  /**
   * Gets the persistence session.
   * @param properties the properties
   * @return the persistence session
   * @throws ServiceException the service exception
   * @throws PersistenceException the persistence exception
   */
  protected abstract PersistenceSession getPersistenceSession(
      Map<String,? extends Serializable> properties) throws ServiceException, PersistenceException;

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#hasRole(org.infodavid.common.services
   * .IApplicationContext , long, org.infodavid.common.model.listitems.IUserGroupListItem.EUserRole)
   */
  @Override
  public boolean hasRole(final IApplicationContext context, final long userKey, final EUserRole role)
      throws ServiceException, PersistenceException {
    if (role == null) {
      throw new IllegalArgumentException(IUserGroup.ROLE);
    }

    final EUserRole highest = findHighestRole(context, userKey);

    return highest.getValue() <= role.getValue();
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#login(java.util.Map, java.lang.String,
   * java.lang.String)
   */
  @Override
  public IApplicationContext signIn(final Map<String,Serializable> properties, final String login,
      final String password) throws PersistenceException, ServiceException {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();

    if (isDebugEnabled) {
      LOGGER.debug("User: " + login + " requests sign in");
    }

    if (StringUtils.isEmpty(login)) {
      throw new IllegalArgumentException(IUser.NAME);
    }

    if (password == null) {
      throw new IllegalArgumentException(IUser.PASSWORD);
    }

    final PersistenceSession persistenceSession = getPersistenceSession(properties);
    final DefaultApplicationContext context =
        new DefaultApplicationContext(properties, persistenceSession);
    // ensure transaction is started
    final ITransaction transaction = persistenceSession.getTransaction();
    final boolean restartTransaction = transaction.isActive();

    // we need to start a transaction
    if (!restartTransaction) {
      transaction.begin();
    }

    IUser user = UserDataService.getInstance().findByName(persistenceSession, login);

    try {
      if (user == null) {
        if (IUser.ADMINISTRATOR.equals(login) || IUser.SYSTEM.equals(login)) {
          final Map<String,IUser> addedUsers = addSystemDataToDbSchema(persistenceSession);
          user = addedUsers.get(login);

          if (!(user == null || password.equals(user.getPassword()))) {
            user = null;
          }
          else {
            persistenceSession.lock(user, ELockType.WAIT, null);
          }
        }

        if (user == null) {
          throw new AccessDeniedException(IUser.USER);
        }
      }
      else {
        persistenceSession.lock(user, ELockType.WAIT, null);

        if (user.getPassword() == null) {
          if (isDebugEnabled) {
            LOGGER.debug("Null password not allowed");
          }

          user = null;
        }
        else if (!user.getPassword().equals(password)) {
          if (isDebugEnabled) {
            LOGGER.debug("Invalid password");
          }

          throw new AccessDeniedException(IUser.USER);
        }
        else if (user.isExpired()) {
          throw new UserAccountExpiredException(IUser.NAME + ':' + login);
        }
        else if (user.isLocked()) {
          throw new UserAccountLockedException(IUser.NAME + ':' + login);
        }
      }

      if (user == null) {
        throw new AccessDeniedException(IUser.USER);
      }

      if (isDebugEnabled) {
        LOGGER.debug("User authenticated = " + user);
      }

      user.setHighestRole(findHighestRole(context, user.getKey()));
      context.setUser(user);

      if (user.getHighestRole() == EUserRole.SYSTEM) {
        throw new AccessDeniedException("Invalid user, system user can not use login");
      }

      LOGGER.info("Sign in of user: " + user.getLoginName());

      if (isDebugEnabled) {
        LOGGER.debug("Updating user profile...");
      }

      final DateTime now = new DateTime();

      user.setConnectionsCount(user.getConnectionsCount() + 1);
      user.setLastConnectionDate(now);
      user.setLastIP(getLastIP(properties));

      // updating user profile
      helper.update(context, user);

      if (isDebugEnabled) {
        LOGGER.debug("Creating user session...");
      }

      final UserSessionDataService userSessionService = UserSessionDataService.getInstance();
      IUserSession userSession =
          userSessionService.findLastActiveByUser(persistenceSession, user.getKey());

      if (userSession == null) {
        try {
          userSession = userSessionService.create();
        }
        catch (final InstantiationException e) {
          throw new ServiceException("Cannot create user session", e);
        }

        CollectionsUtil.getInstance().putAll(properties, userSession.getProperties());
        userSession.setAccessesCount(1);
        userSession.setAuthenticationServer(NetUtil.HOSTNAME);
        userSession.setAuthenticationSource("");
        userSession.setLastAccessDate(user.getLastConnectionDate());
        userSession.setLastIP(user.getLastIP());
        userSession.setLoginName(user.getLoginName());
        userSession.setUser(user);
        userSession.setKey(UUID.nameUUIDFromBytes(
            (user.getLoginName() + System.nanoTime()).getBytes()).toString());

        // inserting user session
        userSessionService.insert(persistenceSession, userSession);
      }
      else {
        persistenceSession.lock(userSession, ELockType.WAIT, null);

        userSession.setAccessesCount(userSession.getAccessesCount() + 1);
        userSession.setLastAccessDate(user.getLastConnectionDate());
        userSession.setLastIP(user.getLastIP());

        // updating user session
        userSessionService.update(persistenceSession, userSession);
      }

      context.setUserSession(userSession);

    }
    finally {
      transaction.commit();

      if (restartTransaction) {
        transaction.begin();
      }
    }

    return context;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#signOut(org.infodavid.common.services
   * .IApplicationContext )
   */
  @Override
  public void signOut(final IApplicationContext context) throws ServiceException,
      PersistenceException {
    if (context != null) {
      final UserSessionDataService userSessionService = UserSessionDataService.getInstance();
      final IUserSession userSession = context.getUserSession();
      final PersistenceSession persistenceSession = context.getPersistenceSession();
      // ensure transaction is started
      final ITransaction transaction = persistenceSession.getTransaction();
      final boolean restartTransaction = transaction.isActive();

      // we need to start a transaction
      if (!restartTransaction) {
        transaction.begin();
      }

      final IVisitor<Collection<IUserSessionListItem>,Object> visitor =
          new UserSessionsRemovalVisitor(context);

      try {
        if (userSession == null) {
          final IUserSessionCriteria criteria = new UserSessionCriteriaImpl();

          // we remove all expired sessions
          criteria.setExpired(Boolean.TRUE);

          userSessionService.reduce(persistenceSession, criteria, visitor, 50);
        }
        else {
          LOGGER.info("Sign in of user: " + userSession.getUser().getLoginName());

          visitor.visit(Collections.singletonList((IUserSessionListItem)userSession));
        }

      }
      catch (final Throwable e) {
        transaction.rollback();

        handle(e, e.getMessage(), this);
      }
      finally {
        transaction.commit();

        if (restartTransaction) {
          transaction.begin();
        }
      }

      context.clear();
    }
  }

  /**
   * Gets the last ip.
   * @param properties the properties
   * @return the last ip
   */
  private String getLastIP(final Map<String,Serializable> properties) {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();
    String result = null;

    if (properties == null) {
      if (isDebugEnabled) {
        LOGGER.debug("Properties: null");
      }
    }
    else {
      if (isDebugEnabled) {
        LOGGER.debug("Properties: " + Arrays.toString(properties.values().toArray()));
      }

      final Serializable value = properties.get(IApplicationContext.IP_ADDRESS_PROPERTY);

      if (value instanceof String) {
        result = (String)value;
      }
    }

    return result;
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.UserService#findByEmail(org.infodavid.common
   * .services.IApplicationContext , java.lang.String)
   */
  @Override
  public IUser findByEmail(final IApplicationContext context, final String value)
      throws PersistenceException, ServiceException {
    if (StringUtils.isEmpty(value)) {
      throw new IllegalArgumentException(IEMail.EMAIL);
    }

    ServiceUtil.validate(context);

    final IInterceptorEvent evt = preExecute(context, FIND_ACTION, value);
    final IUser result =
        ((UserDataService)helper.getDataService()).findByEmail(context.getPersistenceSession(),
            value);

    postExecute(evt.setResults(result));

    return result;
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.UserService#findByName(org.infodavid.common
   * .services.IApplicationContext , java.lang.String)
   */
  @Override
  public IUser findByName(final IApplicationContext context, final String value)
      throws PersistenceException, ServiceException {
    if (StringUtils.isEmpty(value)) {
      throw new IllegalArgumentException(IUser.NAME);
    }

    ServiceUtil.validate(context);

    final IInterceptorEvent evt = preExecute(context, FIND_ACTION, value);
    final IUser result =
        ((UserDataService)helper.getDataService()).findByName(context.getPersistenceSession(),
            value);

    postExecute(evt.setResults(result));

    return result;
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.AbstractEntityService#get(org.infodavid.common .services.
   * IApplicationContext, long)
   */
  @SuppressWarnings("boxing")
  @Override
  public IUser get(final IApplicationContext context, final long key) throws PersistenceException,
      ServiceException {
    helper.validate(context, key);

    final IInterceptorEvent evt = preExecute(context, GET_ACTION, key);
    final IUser result = helper.get(context, Long.valueOf(key));

    if (result != null) {
      result.setHighestRole(findHighestRole(context, key));
    }

    postExecute(evt.setResults(result));

    return result;
  }

  @SuppressWarnings("boxing")
  private EUserRole findHighestRole(final IApplicationContext context, final long key) {
    return findRoles(context, key, new ArrayList<EUserRole>());
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.AbstractEntityService#getEntities(org.infodavid
   * .common.services. IApplicationContext, java.util.List,
   * org.infodavid.common.model.criteria.ISearchCriteria)
   */
  @Override
  public long findEntities(final IApplicationContext context, final List<IUserListItem> entities,
      final IUserCriteria criteria) throws PersistenceException, ServiceException {
    ServiceUtil.validate(context);
    helper.validate(entities);

    final IInterceptorEvent evt = preExecute(context, FIND_ACTION, criteria);
    final long result =
        helper.getDataService().find(context.getPersistenceSession(), entities, criteria);

    postExecute(evt.setResults(entities));

    return result;
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.AbstractEntityService#remove(org.infodavid. common.services.
   * IApplicationContext, long)
   */
  @SuppressWarnings("boxing")
  @Override
  public void remove(final IApplicationContext context, final long key)
      throws PersistenceException, ServiceException {
    helper.validate(context, key);

    final IInterceptorEvent evt = preExecute(context, REMOVE_ACTION, key);
    final IUser existing = helper.get(context, Long.valueOf(key));

    // some users can not be removed
    removalValidator.validate(context, existing);
    helper.remove(context, existing);
    postExecute(evt.setResults(existing));

    LOGGER.info("A user has been removed (" + key + ')');

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("A user has been removed: " + existing + " (" + key + ')');
    }
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.AbstractEntityService#update(org.infodavid. common.services.
   * IApplicationContext, org.infodavid.common.model.IDataObject)
   */
  @Override
  public void update(final IApplicationContext context, final IUser value)
      throws PersistenceException, ServiceException {
    helper.validate(context, getValidator(), value);

    final IInterceptorEvent evt = preExecute(context, UPDATE_ACTION, value);
    final IUser existing =
        helper.getDataService().findByPrimaryKey(context.getPersistenceSession(), value.getKey());
    // data managed by server, can be changed by client
    value.setConnectionsCount(existing.getConnectionsCount());
    value.setLastConnectionDate(existing.getLastConnectionDate());
    value.setLastIP(existing.getLastIP());

    helper.update(context, value);
    postExecute(evt.setResults(value));

    LOGGER.info("A user has been updated (" + value.getKey() + ')');

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("A user has been updated: " + value + " (" + value.getKey() + ')');
    }
  }

  /**
   * Adds the guests groups to the given user.
   * @param context the context
   * @param user the user
   */
  protected void addGuestsGroups(final IApplicationContext context, final IUser user) {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();
    final List<IUserGroup> guests = new ArrayList<IUserGroup>();

    UserGroupDataService.getInstance().findByGuest(context.getPersistenceSession(), true, guests,
        null);

    if (isDebugEnabled) {
      LOGGER.debug("Adding " + guests.size() + " guest groups to user");
    }

    if (guests.isEmpty()) {
      LOGGER.warn("No guest group defined, user " + user.getName()
          + " will not be associated to a group");
    }
    else {
      if (user.getPrimaryGroup() == null) {
        user.setPrimaryGroup(guests.get(0));
      }

      for (final IUserGroup group : guests) {
        user.addGroup(group);
      }
    }
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.UserService#register(org.infodavid.common.services
   * .IApplicationContext , org.infodavid.common.model.IUser)
   */
  @Override
  public long register(final IApplicationContext context, final IUser user)
      throws ServiceException, PersistenceException, NotifierException {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();

    ServiceUtil.validate(context);
    helper.validate(user);

    // we add the guest groups
    addGuestsGroups(context, user);

    // we set the default password
    user.setPassword(getPasswordPolicy().generatePassword());

    add(context, user);

    if (isDebugEnabled) {
      LOGGER.debug("Preparing notification");
    }

    NotificationHelper.getInstance().notifyUserRegistration(context, user);

    LOGGER.info("A user has been registered (" + user.getKey() + ')');

    if (isDebugEnabled) {
      LOGGER.debug("A user has been registered: " + user + " (" + user.getKey() + ')');
    }

    return user.getKey().longValue();
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.UserService#reset(org.infodavid.common.services
   * .IApplicationContext, java.lang.String)
   */
  @Override
  public IUser reset(final IApplicationContext context, final String email)
      throws ServiceException, PersistenceException, NotifierException {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();

    ServiceUtil.validate(context);

    if (StringUtils.isEmpty(email)) {
      throw new IllegalArgumentException(IEMail.EMAIL);
    }

    final IUser user = findByEmail(context, email);

    if (isDebugEnabled) {
      LOGGER.debug("Preparing notification");
    }

    NotificationHelper.getInstance().notifyUserPasswordReset(context, user);

    LOGGER.info("A user has been resetted (" + user.getKey() + ')');

    if (isDebugEnabled) {
      LOGGER.debug("A user has been resetted: " + user + " (" + user.getKey() + ')');
    }

    return user;
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.UserService#changePassword(org.infodavid.common .services.
   * IApplicationContext, long, java.lang.String, java.lang.String)
   */
  @Override
  public IUser changePassword(final IApplicationContext context, final Long key,
      final String oldPassword, final String newPassword) throws ServiceException,
      PersistenceException, NotifierException {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();
    IUser user;

    ServiceUtil.validate(context);
    helper.validate(key);

    getPasswordPolicy().validate(key.longValue(), newPassword);

    user = helper.getDataService().findByPrimaryKey(context.getPersistenceSession(), key);

    if (user == null) {
      throw new NoDataFoundException(IUser.class, key);
    }

    final String password = user.getPassword();

    if (!(password == null && oldPassword == null || password != null
        && password.equals(oldPassword))) {
      throw new AccessDeniedException("Given current password is invalid");
    }

    user.setPassword(newPassword);
    update(context, user);

    if (isDebugEnabled) {
      LOGGER.debug("Preparing notification");
    }

    NotificationHelper.getInstance().notifyUserPasswordChange(context, user);

    if (isDebugEnabled) {
      LOGGER.debug("Password changed for user: " + user + " (" + user.getKey() + ')');
    }

    return user;
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.UserService#findByGroup(org.infodavid.common
   * .services.IApplicationContext , java.lang.Long, java.util.List, java.util.List)
   */
  @Override
  public long findByGroup(final IApplicationContext context, final Long key,
      final List<IUserListItem> entities, final IExtendedSearchCriteria criteria)
      throws ServiceException, PersistenceException {
    ServiceUtil.validate(context);
    helper.validate(entities);

    final IInterceptorEvent evt = preExecute(context, FIND_ACTION, key, criteria);
    final long result =
        ((UserDataService)helper.getDataService()).findByGroup(context.getPersistenceSession(),
            key, entities, criteria);

    postExecute(evt.setResults(entities));

    return result;
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.UserService#findByRole(org.infodavid.common
   * .services.IApplicationContext , org.infodavid.common.model.IGroup.EGroupRole, java.util.List,
   * java.util.List)
   */
  @Override
  public long findByRole(final IApplicationContext context, final EUserRole role,
      final List<IUser> entities, final IExtendedSearchCriteria criteria) throws ServiceException,
      PersistenceException {
    ServiceUtil.validate(context);
    helper.validate(entities);

    final IInterceptorEvent evt = preExecute(context, FIND_ACTION, role, criteria);
    final long result =
        ((UserDataService)helper.getDataService()).findByRole(context.getPersistenceSession(),
            role, entities, criteria);

    postExecute(evt.setResults(entities));

    return result;
  }

  /*
   * See super class or interface. (non-Javadoc)
   * @see org.infodavid.common.services.UserService#findRoles(org.infodavid.common.
   * services.IApplicationContext , java.lang.Long, java.util.List)
   */
  @Override
  public EUserRole findRoles(final IApplicationContext context, final Long userKey,
      final List<EUserRole> results) throws ServiceException, PersistenceException {
    helper.validate(context, userKey);

    final IInterceptorEvent evt = preExecute(context, FIND_ACTION, userKey);
    final Collection<EUserRole> roles =
        ((UserDataService)helper.getDataService()).findRoles(context.getPersistenceSession(),
            userKey);
    EUserRole result = EUserRole.NORMAL;

    if (!(roles == null || roles.isEmpty())) {
      results.addAll(roles);

      for (final EUserRole item : roles) {
        if (item.getValue() < result.getValue()) {
          result = item;
        }
      }
    }

    postExecute(evt.setResults(helper.get(context, userKey)));

    return result;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#findConnections(org.infodavid.common .services.
   * IApplicationContext, java.util.List, org.infodavid.common.model.criteria.IUserConnectionCriteria)
   */
  @Override
  public long findConnections(final IApplicationContext context,
      final List<IUserConnectionListItem> entities, final IUserConnectionCriteria criteria)
      throws ServiceException, PersistenceException {
    ServiceUtil.validate(context);
    connectionHelper.validate(entities);

    final IInterceptorEvent evt = preExecute(context, FIND_ACTION, criteria);
    final long result =
        connectionHelper.getDataService().find(context.getPersistenceSession(), entities, criteria);

    postExecute(evt.setResults(entities));

    return result;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#isConnectionOf(org.infodavid.common .services.
   * IApplicationContext, java.lang.Long, java.lang.Long)
   */
  @Override
  public boolean isConnectionOf(final IApplicationContext context, final Long connectionKey,
      final Long userKey) throws ServiceException, PersistenceException {
    helper.validate(context, userKey);

    if (connectionKey == null) {
      throw new IllegalArgumentException(IUserConnection.RECIPIENT);
    }

    return UserConnectionDataService.getInstance().isConnectionOf(context.getPersistenceSession(),
        connectionKey, userKey);
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#submitConnections(org.infodavid .common.services.
   * IApplicationContext, java.util.Collection)
   */
  @Override
  public void submitConnections(final IApplicationContext context,
      final Collection<IUserConnection> connections) throws ServiceException, PersistenceException,
      NotifierException {
    ServiceUtil.validate(context);

    if (!(connections == null || connections.isEmpty())) {
      final PersistenceSession session = context.getPersistenceSession();
      final UserConnectionDataService connectionService = UserConnectionDataService.getInstance();
      final NotificationHelper notificationHelper = NotificationHelper.getInstance();

      for (final IUserConnection item : connections) {
        if (isConnectionOf(context, item.getInitiator().getKey(), item.getRecipient().getKey())) {
          LOGGER.debug("User connection already exists: " + item);
        }
        else {
          item.setApprovalStatus(EApprovalStatus.WAITING_APPROVAL);
          item.setInitiator(context.getUser());

          connectionService.insert(session, item);
          notificationHelper.notifyUserConnectionSubmitted(context, item);
        }
      }
    }
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#updateConnections(org.infodavid .common.services.
   * IApplicationContext, java.util.Collection)
   */
  @Override
  public void updateConnections(final IApplicationContext context,
      final Collection<IUserConnection> connections) throws ServiceException, PersistenceException,
      NotifierException {
    ServiceUtil.validate(context);

    if (!(connections == null || connections.isEmpty())) {
      final NotificationHelper notificationHelper = NotificationHelper.getInstance();
      IUserConnection existing;

      for (final IUserConnection item : connections) {
        existing = connectionHelper.get(context, item.getKey());

        connectionHelper.update(context, item);

        if (existing.getApprovalStatus() != EApprovalStatus.APPROVED
            && item.getApprovalStatus() == EApprovalStatus.APPROVED) {
          notificationHelper.notifyUserConnectionChanges(context, item);
        }
      }
    }
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#findSessions(org.infodavid.common .services.
   * IApplicationContext, java.util.List, org.infodavid.common.model.criteria.IUserSessionCriteria)
   */
  @Override
  public long findSessions(final IApplicationContext context,
      final List<IUserSessionListItem> entities, final IUserSessionCriteria criteria)
      throws ServiceException, PersistenceException {
    ServiceUtil.validate(context);
    sessionHelper.validate(entities);

    final IInterceptorEvent evt = preExecute(context, FIND_ACTION, criteria);
    final long result =
        sessionHelper.getDataService().find(context.getPersistenceSession(), entities, criteria);

    postExecute(evt.setResults(entities));

    return result;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#getConnection(org.infodavid.common .services.
   * IApplicationContext, long)
   */
  @SuppressWarnings("boxing")
  public IUserConnection getConnection(final IApplicationContext context, final long key)
      throws PersistenceException, ServiceException {
    helper.validate(context, key);

    final IInterceptorEvent evt = preExecute(context, GET_ACTION, key);
    final IUserConnection result = connectionHelper.get(context, Long.valueOf(key));

    postExecute(evt.setResults(result));

    return result;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#removeConnection(org.infodavid. common.services.
   * IApplicationContext, long)
   */
  @SuppressWarnings("boxing")
  @Override
  public void removeConnection(final IApplicationContext context, final long key)
      throws PersistenceException, ServiceException {
    helper.validate(context, key);

    final IInterceptorEvent evt = preExecute(context, REMOVE_ACTION, key);
    final IUserConnection existing = connectionHelper.get(context, Long.valueOf(key));

    connectionHelper.remove(context, existing);
    postExecute(evt.setResults(existing));

    LOGGER.info("A user connection has been removed (" + key + ')');

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("A user connection has been removed: " + existing + " (" + key + ')');
    }
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#findProperties(org.infodavid.common .services.
   * IApplicationContext, java.util.Collection, long,
   * org.infodavid.common.model.criteria.IGenericPropertyCriteria)
   */
  @Override
  public void findProperties(final IApplicationContext context,
      final Collection<IGenericProperty> entities, final long userKey,
      final IGenericPropertyCriteria criteria) throws PersistenceException, ServiceException {
    ServiceUtil.validate(context);
    ServiceUtil.validate(entities);

    final IInterceptorEvent evt = preExecute(context, FIND_ACTION, criteria);

    ((UserDataService)helper.getDataService()).findProperties(context.getPersistenceSession(),
        entities, userKey, criteria);
    postExecute(evt.setResults(entities));
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.services.UserService#updateProperties(org.infodavid. common.services.
   * IApplicationContext, long, java.util.Collection)
   */
  @Override
  public DateTime updateProperties(final IApplicationContext context, final long userKey,
      final Collection<IGenericProperty> properties) throws PersistenceException, ServiceException {
    ServiceUtil.validate(context);
    ServiceUtil.validate(properties);

    final IInterceptorEvent evt = preExecute(context, UPDATE_ACTION, properties);

    final DateTime modificationDate =
        ((UserDataService)helper.getDataService()).updateProperties(
            context.getPersistenceSession(), userKey, properties);
    postExecute(evt.setResults(properties));

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(properties.size() + " properties has been updated");
    }

    return modificationDate;
  }
}
